// 15 面向对象程序设计
/**
 * 面向对象程序设计基于三个基本概念：数据抽象、继承和动态绑定。第7章已经介绍了数据抽象的知识，本章将介绍继承和动态绑定。
 * 继承和动态绑定对程序的编写有两方面的影响：一是我们可以更容易地定义与其他类相似但不完全相同的新类；二是在使用这些彼此相似的类编写程序时，我们可以在一定程度上忽略掉它们的区别。
 * 在很多程序中都存在着一些相互关联但是有细微差别的概念。
 * 例如，书店中不同书籍的定价策略可能不同：有的书籍按原价销售，有的则打折销售。有时，我们给那些购买书籍超过一定数量的顾客打折；另一些时候，则只对前多少本销售的书籍打折，之后就调回原价，等等。
 * 面向对象的程序设计（OOP）适用于这类应用。
 */

#include <iterator>
#include <vector>
#include <list>
#include <deque>
#include <forward_list>
#include <string>
#include <array>
#include <stack>
#include <queue>
#include <algorithm>
#include <numeric>
using std::swap;
using std::vector, std::list, std::deque, std::forward_list, std::string, std::array, std::stack, std::queue;
#include "../Chapter07/Sales_data.h"
#include <iostream>
using std::begin, std::cbegin, std::end, std::cend, std::find, std::accumulate, std::equal, std::fill, std::fill_n, std::back_inserter;
using std::cin, std::cout, std::endl;
using std::copy, std::replace, std::replace_copy;
#include <map>
#include <set>
#include <unordered_map>
#include <unordered_set>
#include <utility>
#include <memory>
#include <new>
using namespace std;
#include "../Chapter13/13.5.cc" // 不能编译，因为重复定义的main函数
#include "../Chapter12/12.1.6.cc" // 不能编译，因为重复定义的main函数
#include <functional>

// 为了对之前提到的不同定价策略建模，我们首先定义一个名为Quote的类，并将它作为层次关系中的基类。
// Quote的对象表示按原价销售的书籍。Quote派生出另一个名为Bulk_quote的类，它表示可以打折销售的书籍。
class Quote
{
public:
    Quote() = default; // 关于=default参见7.1.4节（第237页）
    Quote(const string &book, double sales_price) : bookNo(book), price(sales_price) {}
    // isbn（ ），返回书籍的ISBN编号。该操作不涉及派生类的特殊性，因此只定义在Quote类中。
    std::string isbn() const { return bookNo; };
    // net_price（size_t），返回书籍的实际销售价格，前提是用户购买该书的数量达到一定标准。这个操作显然是类型相关的，Quote和Bulk_quote都应该包含该函数。
    virtual double net_price(std::size_t n) const { return n*price; };
    // 在C++语言中，基类将类型相关的函数与派生类不做改变直接继承的函数区分对待。对于某些函数，基类希望它的派生类各自定义适合自身的版本，此时基类就将这些函数声明成虚函数（virtual function）。
    virtual ~Quote() = default; // 对析构函数进行动态绑定。根节点的类通常都会定义一个虚析构函数。
private:
    std::string bookNo; // 书籍的ISBN编号
protected:
    double price = 0.0; // 代表普通状态下不打折的价格
};
// 派生类必须通过使用类派生列表（class derivation list）明确指出它是从哪个（哪些）基类继承而来的。
// 类派生列表的形式是：首先是一个冒号，后面紧跟以逗号分隔的基类列表，其中每个基类前面可以有访问说明符：
class Bulk_quote : public Quote // Bulk_quote继承了Quote
{
public:
    Bulk_quote() = default;
    Bulk_quote(const string &book, double p, size_t qty, double disc) : Quote(book,p), min_qty(qty), discount(disc) {}; // 使用其基类构造函数初始化基类成员
    // 覆盖基类的函数版本以实现基于大量购买的折扣策略
    double net_price(std::size_t) const override; // 形参列表后增加override关键字
private:
    std::size_t min_qty = 0; // 使用折扣策略的最低购买量
    double discount = 0.0;   // 以小数表示的折扣额
};
// 计算并打印销售给定数量的某种书籍所得的费用
double print_total(ostream &os, const Quote &item, size_t n)
{
    // 根据传入item形参的对象类型调用Quote::net_price或者Bulk_quote::net_price
    double ret = item.net_price(n);
    os << "ISBN: " << item.isbn() // 调用Quote::isbin
       << " # sold: " << n << " total due: " << ret << endl;
    return ret;
}
// 如果达到了购买书籍的某个最低限量值，就可以享受折扣价格了
double Bulk_quote::net_price(std::size_t cnt) const
{
    if(cnt > min_qty)
        return cnt * (1-discount) * price;
    else
        return cnt * price;
}

int main()
{
    // 15.3 虚函数
    // 如前所述，在C++语言中，当我们使用基类的引用或指针调用一个虚成员函数时会执行动态绑定（参见15.1节，第527页）。
    // 因为我们直到运行时才能知道到底调用了哪个版本的虚函数，所以所有虚函数都必须有定义。
    // 通常情况下，如果我们不使用某个函数，则无须为该函数提供定义（参见6.1.2节，第186页）。
    // 但是我们必须为每一个虚函数都提供定义，而不管它是否被用到了，这是因为连编译器也无法确定到底会使用哪个虚函数。

    // 对虚函数的调用可能在运行时才被解析
    // 当某个虚函数通过指针或引用调用时，编译器产生的代码直到运行时才能确定应该调用哪个版本的函数。
    Quote base("0-201-82470-1", 50);
    print_total(cout, base, 10); // 调用Quote::net_price
    Bulk_quote derived("0-201-82470-1", 50, 5, .19);
    print_total(cout, derived, 10); // 调用Bulk_quote::net_price
    // 必须要搞清楚的一点是，动态绑定只有当我们通过指针或引用调用虚函数时才会发生。
    base = derived;     // 把derived的Quote部分拷贝给base
    base.net_price(10); // 调用Quote::net_price
    // 当我们通过一个具有普通类型（非引用非指针）的表达式调用虚函数时，在编译时就会将调用的版本确定下来。

    // 关键概念：C++的多态性
    // OOP的核心思想是多态性（polymorphism）。多态性这个词源自希腊语，其含义是“多种形式”。
    // 我们把具有继承关系的多个类型称为多态类型，因为我们能使用这些类型的“多种形式”而无须在意它们的差异。
    // 引用或指针的静态类型与动态类型不同这一事实正是C++语言支持多态性的根本所在。
    // 当我们使用基类的引用或指针调用基类中定义的一个函数时，我们并不知道该函数真正作用的对象是什么类型，因为它可能是一个基类的对象也可能是一个派生类的对象。
    // 当且仅当对通过指针或引用调用虚函数时，才会在运行时解析该调用，也只有在这种情况下对象的动态类型才有可能与静态类型不同。

    // 派生类中的虚函数
    // 当我们在派生类中覆盖了某个虚函数时，可以再一次使用virtual关键字指出该函数的性质。然而这么做并非必须，因为一旦某个函数被声明成虚函数，则在所有派生类中它都是虚函数。
    // 基类中的虚函数在派生类中隐含地也是一个虚函数。当派生类覆盖了某个虚函数时，该函数在基类中的形参必须与派生类中的形参严格匹配。

    // final和override说明符
    // 如我们将要在15.6节（第550页）介绍的，派生类如果定义了一个函数与基类中虚函数的名字相同但是形参列表不同，这仍然是合法的行为。
    // 就实际的编程习惯而言，这种声明往往意味着发生了错误，因为我们可能原本希望派生类能覆盖掉基类中的虚函数，但是一不小心把形参列表弄错了。
    // 在C++11新标准中我们可以使用override关键字来说明派生类中的虚函数。这么做的好处是在使得程序员的意图更加清晰的同时让编译器可以为我们发现一些错误，后者在编程实践中显得更加重要。
    // 因为只有虚函数才能被覆盖
    // 我们还能把某个函数指定为final，如果我们已经把函数定义成final了，则之后任何尝试覆盖该函数的操作都将引发错误：
    // final和override说明符出现在形参列表（包括任何const或引用修饰符）以及尾置返回类型（参见6.3.3节，第206页）之后。

    // 虚函数与默认实参
    // 和其他函数一样，虚函数也可以拥有默认实参（参见6.5.1节，第211页）。
    // 如果虚函数使用默认实参，则基类和派生类中定义的默认实参最好一致。

    // 回避虚函数的机制
    // 在某些情况下，我们希望对虚函数的调用不要进行动态绑定，而是强迫其执行虚函数的某个特定版本。使用作用域运算符可以实现这一目的。
    Bulk_quote bulk;
    Quote *baseP = &bulk;
    double undiscounted = baseP->Quote::net_price(10); // 强行调用基类中定义的函数版本，而不管baseP的动态类型到底是什么
    // 通常情况下，只有成员函数（或友元）中的代码才需要使用作用域运算符来回避虚函数的机制。
    // 什么时候我们需要回避虚函数的默认机制呢？通常是当一个派生类的虚函数调用它覆盖的基类的虚函数版本时。
    // 在此情况下，基类的版本通常完成继承层次中所有类型都要做的共同任务，而派生类中定义的版本需要执行一些与派生类本身密切相关的操作。
    // 如果一个派生类虚函数需要调用它的基类版本，但是没有使用作用域运算符，则在运行时该调用将被解析为对派生类版本自身的调用，从而导致无限递归。


    return 0;
}